feat(interest-agent): conversational onboarding UI - #6549
Conversation
Renders the new onboarding turns inside the existing transcript, so setting an agent up is the same surface as talking to it afterwards. Pairs with daily-api #interest-agent-onboarding. - question, brief and review block renderers. Questions are chips or free text, live only at the tail; the brief can be rewritten inline before it is accepted; the review card edits settings in place and starts the first hunt. - The composer stays mounted but is disabled unless an open question actually wants prose, so nothing can produce feedback no question is waiting on. - Enter advances whichever step is open, via one shared advanceOnboarding in the context. It is handled at the workspace level because a disabled field fires no keydown and clicking a chip moves focus off the composer. - Onboarding reads as "Setting up" in the intro and the agent list rather than falling through to "Paused". - Delivery/output-mode controls are gone from onboarding and settings; notifications stays as its own step and its own setting. - isPostsBlock guard: collecting posts from blocks assumed anything not text carries posts, which the new block types broke at runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
capJavert
left a comment
There was a problem hiding this comment.
Summary
Solid shape overall: one renderer per block type, the API owning the question list, and the isPostsBlock guard is the right fix for the .posts crash. The risk in this PR is concentrated in the workspace-wide Enter handler and in the new onboarding status leaking into surfaces outside the diff. Inline comments cover the per-line items; these are the cross-cutting ones.
Blocking
Deploy order, not just merge order. USER_INTEREST_FRAGMENT now requests brief and onboardingStep, and that fragment backs every interest read (list, single, create, update). If this ships before dailydotdev/daily-api#4169 is deployed, GraphQL validation fails on the fragment itself and the whole agent surface breaks, not just onboarding — same for $onboarding on createInterest and $questionId on sendInterestCommand. The description says merge the API first; worth confirming it is live in production before this merges, since a merge-order-only guarantee does not cover the gap.
Blast radius of the new onboarding status. Two places outside the diff still read it as "not active":
components/AgentSettingsMenu.tsx:51—isRunning = status === UserInterestStatus.Active, so during setup the header menu shows "Resume the agent" with the switch off, and toggling it writesstatus: active. That drops the user out of onboarding mid-transcript whileonboardingStepstill points at an open step; afterwardsactiveQuestion/isReviewOpenare both false, the chips are dead, and there is no route back into setup. The switch should be hidden or disabled while onboarding.components/AgentMark.tsx:19(agentStateLabel, viaAgentStatusTilein the workspace header) — reads "Paused" during onboarding, and the dot goes grey. That is the same fall-through the PR removes inAgentIntroandmonitorItems, on the header the user is looking at during setup.
Non-blocking
- Output modes removed with no path for existing agents. With
OutputModesSectiondeleted,feed,postanddigestare no longer editable anywhere. An agent created earlier with the digest email on keeps sending it and the user has no UI to turn it off. Is the API migrating those, or should the notifications section still expose the digest toggle? - Tests. The new routing (
advanceOnboardingacross question/brief/review,pendingAnswerseeding and reset, chips vs typed answers) is the risky surface here and has no spec; the two updated specs only assert the create payload.AgentContext.spec.tsxlooks like the natural home for a couple of cases.
Verified
CI green on all checks; no merge conflicts; scope is coherent; shared-package consumers of the changed components enumerated; isPostsBlock covers every remaining .posts reader (attachments.ts, replyText.ts). Not verified: behaviour against the API branch, and the flow on a touch device.
Reviewed by AI.
| } | ||
| }; | ||
|
|
||
| globalThis.addEventListener('keydown', onKeyDown); |
There was a problem hiding this comment.
Blocking: this handler swallows Enter on every focused button in the workspace.
For a focused <button>, keydown fires first and the click is its default action, so event.preventDefault() here cancels the button's own activation. The guard only exempts INPUT/TEXTAREA/contenteditable/dialog, so a keyboard user during onboarding gets:
- Tab to Edit it → Enter runs
confirmBrief()and accepts the brief instead of opening the editor. - Tab to Save and continue after a rewrite →
advanceOnboarding()callsconfirmBrief()with no argument, so the original brief is saved and the rewrite is dropped; the button'sonClicknever runs. - Tab to a Change row in the review card → Enter completes onboarding and starts the first hunt instead of expanding the row.
- Tab to a single-select chip → the seeded
pendingAnsweris submitted, not the focused chip.
It also captures the header buttons (Agent settings, close, tabs) for as long as an onboarding step is open.
Suggested direction: bail when the target is interactive, e.g. if (target?.closest('button, a, select, [role="button"], [contenteditable]')) { return; }. Those controls already invoke the same actions on activation, so nothing is lost and the "press Enter from anywhere" behaviour still holds for the empty-focus case.
Reviewed by AI.
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isTailPending]); | ||
|
|
||
| // "Press Enter" has to hold anywhere on the screen: clicking a chip moves |
There was a problem hiding this comment.
Non-blocking: useKeyboardNavigation(globalThis?.window, [['Enter', handler]], { disableOnTags: ['input', 'textarea'] }) already exists in src/hooks/useKeyboardNavigation.ts and is used two files over in AgentContentPane.tsx:264. It does the tag guard, the contenteditable check and the special-key/modifier filter for you. Any reason not to reuse it here rather than hand-rolling the listener?
Reviewed by AI.
| size={ButtonSize.Small} | ||
| variant={ButtonVariant.Primary} | ||
| loading={isConfirmingBrief} | ||
| onClick={() => confirmBrief()} |
There was a problem hiding this comment.
Blocking: an open editor is invisible to advanceOnboarding, so a rewrite can be silently discarded.
draft is local to this component. While the textarea is open, any Enter with focus outside it — the user clicked the card background, or tabbed to a button — reaches the workspace handler, hits the isBriefOpen branch and calls confirmBrief() with no argument. The original brief is accepted, the draft disappears with no toast and no confirmation, and the flow moves to the review step.
Either lift the editing state into the context so advanceOnboarding can skip (or submit) it, or let the block register its own submit while editing.
Reviewed by AI.
| !lastMessage.isPending && | ||
| !!lastMessage.blocks?.some((block) => block.type === 'review'); | ||
|
|
||
| const isBriefOpen = |
There was a problem hiding this comment.
Non-blocking: activeQuestion and isReviewOpen are both gated on isOnboarding, isBriefOpen is not. If the API leaves onboardingStep at brief after completeInterestOnboarding — or on a paused/stopped agent — the global Enter keeps confirming the brief and the brief card keeps its action buttons on a live agent. Making it isOnboarding && interest?.onboardingStep === UserInterestOnboardingStep.Brief costs nothing and stops the client depending on the API nulling that column.
Reviewed by AI.
| className="shrink-0" | ||
| disabled={!feedback.trim() && !command} | ||
| disabled={ | ||
| isComposerOff || (!feedback.trim() && !command && !canAdvance) |
There was a problem hiding this comment.
Non-blocking: on a free-text question canAdvance is true and isComposerOff is false, so the send button is enabled with an empty field — but onSubmit takes the !trimmed branch, and advanceOnboarding() returns false for a text question (choices is undefined, so picked is empty). The button is live and does nothing, with no feedback.
Related, for the same surface on touch: there is no Enter key there, so Press Enter to start hunting… and the or press Enter hints are dead copy on a phone. The chips, Looks right and Start hunting cover the flow, so it is only the copy and this one enabled-but-inert button that mislead. Worth either disabling send when a text question has no input, or routing send through the same path Enter takes.
Reviewed by AI.
Client half of conversational agent onboarding. Pairs with dailydotdev/daily-api#4169 — merge that first.
Setting an agent up now happens in the same transcript you talk to it in afterwards, rather than a separate form.
What's here
Three block renderers.
question(chips or free text, live only at the transcript tail — earlier ones dim to a ✓),brief(rewritable inline before it's accepted), andreview(edits settings in place, then starts the first hunt).No settings wizard. Settings questions arrive as the same
questionblocks the agent authors, so the client owns neither the list, the order, nor the "load my recent settings" logic — the API does. One renderer covers the whole flow and a reload resumes from the transcript.Composer stays mounted but disabled unless an open question actually wants prose. Everything else is driven by its own controls, so the UI can't produce feedback no question is waiting on.
Enter advances whichever step is open, through one shared
advanceOnboardingin the context. It's handled at the workspace level rather than on the composer, because a disabled field fires nokeydownand clicking a chip moves focus off the composer — the reason the earlier version of this didn't work.Status.
onboardingreads as "Setting up" in the intro and the agent list instead of falling through to "Paused".Also in here
OutputModesSectionhad no callers left and is deleted.0.2 / 0.5 / 0.8 / 0.95) and the settings slider's copy now bands to match, so it never describes a value differently from the chip that set it.attachments.tsandreplyText.tscollected posts from blocks by assuming anything that isn'ttextcarriesposts. The new block types made that a runtime crash on the composer's mention candidates. Replaced with anisPostsBlockguard, so a future block type is excluded by default instead of breaking whatever reads.posts.Testing
267 interest specs pass; strict typecheck across the whole
sharedpackage (not just changed files — that's what let the.postscrash through) and lint are clean. Walked the flow locally against the API branch. TwoAgentHomeScreenspecs updated for theonboarding: truepayload and the removed digest switch.🤖 Generated with Claude Code
Preview domain
https://feat-interest-agent-onboarding.preview.app.daily.dev